Skip to content

fix(compilers/openapi): report an oauth2 scheme with no flow - #727

Open
fuad-daoud wants to merge 4 commits into
mainfrom
fix/oauth2-no-flows
Open

fuad-daoud wants to merge 4 commits into
mainfrom
fix/oauth2-no-flows

Conversation

@fuad-daoud

@fuad-daoud fuad-daoud commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

{type: oauth2, flows: {}} is structurally valid — every member of OAuthFlows is optional — so it lowers to an AuthKindOAuth2 scheme with an empty flow list and no report from anywhere. An oauth2 scheme states no token endpoint without a flow, so the entry is unusable, and a requirement naming it keeps a live AuthID for a scheme that says nothing an emitter can implement (#646).

Rule. An oauth2 entry whose flows object is present but names no flow (flows: {}, or only keys this model does not name), and which has no oauth2MetadataUrl, gets one warning at its declaration. An absent flows is left to the loader's required-field error.

Code, severity, placement. New code openapi/oauth2-no-flow (diag.OAuth2NoFlow), warning, at the declaration, once however many aliases reach it. The loader accepts flows: {}, so only the compiler can see that shape. The spelling the loader does refuse, flows absent, it reports as validation-required-field at a line and column. Reporting that again would give one defect two diagnostics and two severities, and make the exit code depend on the spelling. A flowless declaration is a fact about the text where its fields are written, and a {$ref: …} entry holds no flows either way. So the report sits at decl, and its message names no entry, which lets the copies every alias draws dedup to one.

The scheme still interns — deliberately not the refusal #294 points at. #294 refused an entry naming no mechanism because ir.AuthKind has no value for "the document did not say which mechanism": interning would have asserted that the API is authenticated by nothing in particular. Here the IR states exactly what the document said — oauth2, with no flows — so nothing is misstated and nothing must be refused. Refusing would also delete declared text (description, x-*, oauth2MetadataUrl) and collapse every requirement naming the scheme, for a document defect the IR can hold. The compiler already interns the two equally unusable siblings — apiKey with no name, openIdConnect with no URL — so wire-level usability is not the line it draws; the mechanism token is (fillSchemeKind), and that line is unchanged.

Warning, not error. The document lowers whole, and the shape is the same class as DisjointVisibility (exactly represented, unusable), EmptyEnum, ReservedHeaderName, InvalidMethodKey and InvalidLocationKeyword. An error would also stop harness.Check at the first error diagnostic and keep any fixture carrying the shape out of irverify; --fail-on warning is the caller's lever.

The metadata URL exempts the entry. In 3.2.0 oauth2MetadataUrl points at RFC 8414 authorization-server metadata, which states the token endpoint, and flows is REQUIRED, so {type: oauth2, flows: {}, oauth2MetadataUrl: …} is the only way to write a valid metadata-only scheme. The exemption follows the IR field, so it also applies when a 3.1 document carries the URL, since the library reads it there too.

Where it lives.

  • compilers/openapi/internal/diag/diag.go: diag.OAuth2NoFlow with its design record, immediately after IncompleteSecurityScheme.
  • compilers/openapi/internal/auth/auth.go: declaresNoFlow (the predicate) and oauthNoFlowDiag beside mechanismRefusalDiag, and the branch after the named guard in lowerSecurityScheme. fillSchemeKind's oauth2 case records that an absent or empty flows object lowers to nil flows, and LowerSecuritySchemes' doc names the reporter of an entry that did intern.
  • ir/auth.go: AuthKindOAuth2 now documents zero or more flows, since a flowless scheme is a shape the compiler deliberately produces. No ir/ type changes, no Options switch, no irverify rule: nothing is inferred, and holding flow fields in irverify would re-validate the source.

Test plan

  • TestLowerSecuritySchemes_AnOAuth2SchemeWithNoFlowIsReported pins the rule row by row:

    • flows: {} (the issue repro): one warning at /components/securitySchemes/s.
    • flows absent: no oauth2-no-flow, only the loader's error.
    • flows: {application: …}: the warning, plus the existing unknown-object-key at /components/securitySchemes/s/flows/application.
    • flows: {} with an oauth2MetadataUrl under 3.2.0: silent, and the URL is kept.
    • One declared flow, and one declared device flow: silent.
    • An apiKey scheme carrying flows: {}: silent, with the stray field kept. This row pins the Kind guard.
    • {flows: {}} with no type: only incomplete-security-scheme.

    Every interned row also pins that the requirement naming the scheme keeps a live AuthID.

  • TestLowerSecuritySchemes_AFlowlessDeclarationIsReportedOnceWhereItIsWritten: two $ref aliases, declared before the flowless target they share, all intern, and the one declaration is reported once at /components/securitySchemes/target, with nothing at either alias.

  • TestAuth_OAuthNoFlowsUnknownTypeAndGhostRef asserts the absent spelling draws no oauth2-no-flow report and that the loader still reports it.

  • TestPresentFlows_ReadsEverySourceFlowField sets each flow field of soa.OAuthFlows alone, by reflection, and requires presentFlows to read it. The report reads an empty lowered list as "declares no flow", which holds only while every flow field is read. Commenting out the password flow reddens it.

  • New conformance spec testdata/conformance/openapi/oauth2-no-flow.yaml, with its golden and table row. It holds flows: {}, a flows object with only an unknown key, two aliases declared before their flowless target, and two silent controls: a 3.2.0 metadata-only scheme and a declared flow. morphic-harness passes on it, so the corpus oracles now reach the rule.

  • Mutations, each planted, observed and reverted:

    • delete the branch (if false && …) → TestConformance, the placement test and the table test redden;
    • drop the Kind guard → the table test's apiKey row reddens;
    • drop the absent-flows check → TestAuth_OAuthNoFlowsUnknownTypeAndGhostRef and the table test redden;
    • drop the metadata exemption → TestConformance and the table test redden;
    • report at entry instead of decl → TestConformance and the placement test redden.
  • go test ./... and make lint (0 issues) on b87282c.

  • make gate on b87282c (fmt, vet, lint, build, coverage at 100%, fuzz, bench-smoke) passes.

Deliberately out of scope

  • A flow that is present but incomplete (missing tokenUrl, authorizationUrl or scopes): that is the loader's own finding, and not what "declares no flow" means.
  • An irverify rule: holding flow fields there would re-validate the source, which its own doc rules out.
  • The library's securityScheme.flows is required finding for an absent flows is left exactly as it is, and is the only report of that spelling. Owning every spelling in the loader would need a node-based predicate, since the library error has no structured field identity. That belongs in a separate change.

Closes #646.

@fuad-daoud fuad-daoud self-assigned this Sep 30, 2026
@fuad-daoud fuad-daoud changed the title fix(compilers/openapi): report an oauth2 scheme with no flow (#646) fix(compilers/openapi): report an oauth2 scheme with no flow Sep 30, 2026
Resolve the conflicts with main and bring the doc comments this branch
adds under the 100-word cap main now enforces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCzyGPq9DQ7RLfagZm2JB5

@OmarAlJarrah OmarAlJarrah left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected after a second pass: I don't think this reaches the bar yet, contrary to my first summary. The verdict and the ordered list of gaps are in the PR comment below, and each point is detailed inline.

One item outside the diff: the merge commit 97b5fb2 carries an AI co-author trailer and a session URL in its body. The branch shouldn't be rewritten, so set the squash body explicitly at merge time rather than letting a COMMIT_MESSAGES paste carry it onto main.

Comment thread compilers/openapi/internal/auth/auth.go Outdated
func oauthNoFlowDiag(c lowering.Ctx, name string, entry jsontext.Pointer) ir.Diagnostic {
return c.DiagAt(ir.SeverityWarning, diag.OAuth2NoFlow, entry,
"security scheme %q is an oauth2 scheme with no flow: "+
"no OAuth flow is interned for it, and an OAuth2 scheme states no token endpoint without one", name)

@OmarAlJarrah OmarAlJarrah Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule behind this message reports a valid, usable document. In OpenAPI 3.2.0, oauth2MetadataUrl is "URL to the OAuth2 authorization server metadata [RFC8414]", and RFC 8414 metadata states token_endpoint and authorization_endpoint. flows is REQUIRED in 3.2.0, so {type: oauth2, flows: {}, oauth2MetadataUrl: ...} is the only way to write a metadata-only scheme. Compiled under openapi: 3.2.0 it prints this warning and exits 0, and --fail-on warning then rejects a spec-valid document. #646's own premise is a scheme that "describes no way to obtain a token"; a metadata URL is one.

The PR body and diag.OAuth2NoFlow record that the URL deliberately does not exempt the entry. I disagree on the merits and would reverse it: a warning that fires on valid input that says how to get a token is a false positive, not a stricter lint. The fix is one more clause, scheme.OAuth2MetadataURL == "". On a prototype, a 3.2.0 metadata-only scheme is silent and flows: {} without the URL still reports.

The flows empty with a metadata url row would then become the silent control. It compiles under openapi: 3.1.0 today, a version that doesn't define the field, so it should compile under 3.2.0.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and reversed. declaresNoFlow now returns false when oauth2MetadataUrl is set, so a 3.2.0 metadata-only scheme is silent, and flows: {} without the URL still reports. The flows empty with a 3.2 metadata url row now compiles under openapi: 3.2.0 as the silent control, and the new conformance spec carries the same control (metadataOnly). Dropping the exemption reddens both. b87282c

Comment thread compilers/openapi/internal/diag/diag.go Outdated
// OAuth2NoFlow reports an oauth2 securitySchemes entry declaring no flow:
// `flows` absent, `flows: {}`, or only keys the model does not name, which all
// lower to an empty flow list. It is reported once, at the entry's pointer,
// because the loader cannot place it: it accepts `flows: {}` and reports an

@OmarAlJarrah OmarAlJarrah Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence is more than imprecise; its premise undercuts the placement rationale. The loader's finding does carry a site: morphic validate on {type: oauth2} prints absent.yaml:7:7: ... securityScheme.flows is required for type=oauth2, a Position with no pointer, and #511 made line:col a first-class locator beside the pointer. So for the absent spelling "the loader cannot place it" is false, and the new warning is a second report of a defect the loader already reports as an error.

That leaves one defect with two diagnostics, two severities and an exit code that depends on the spelling: absent exits 1, flows: {} exits 0. That is the opposite of the PR body's "one rule for every spelling". The spellings are not alike in the spec. Absent violates a REQUIRED field, so an error is right and the loader supplies it. flows: {} is valid but unusable, so a warning is right and only the compiler can see it. The repo's precedent when a rule could be checked in both stages is compilerOwned (#502): one report, not two.

The consistent design reports only what the loader cannot see: ss.GetFlows() != nil && len(scheme.Flows) == 0. GetFlows() is nil for an absent key and non-nil for {}, so the lowering can tell them apart even though the IR cannot (the PR's stated reason for one rule). Prototype result: absent draws only the loader's error, and {} and a flows object holding only unknown keys draw the one warning. The bigger alternative is owning the whole rule in the loader (drop the library finding, report every spelling pointed). That needs a node-based predicate, since the library error has no structured field identity, and I would file it separately.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed: the loader does site the absent spelling, by line and column (absent.yaml:6:8: securityScheme.flows is required for type=oauth2), so the old sentence was wrong. The warning now fires only for what the loader cannot see, ss.GetFlows() != nil with no lowered flow. {type: oauth2} now draws only the loader's error, while {} and a flows object holding only unknown keys draw the one warning. diag.OAuth2NoFlow's record is rewritten to match. Moving the whole rule into the loader is left as the separate change you describe. b87282c

Comment thread compilers/openapi/internal/auth/auth.go Outdated
diags = preserveUnreadFields(c, &scheme, ss, decl)
if scheme.Kind == ir.AuthKindOAuth2 && len(scheme.Flows) == 0 {
// Reported rather than refused — see oauthNoFlowDiag — and reported
// before the lines below, which is why those append into diags.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "reported before the lines below, which is why those append into diags" describes the diff (only preserveUnreadFields changed from assignment to append) and reads as an ordering requirement the code doesn't have. "Reported rather than refused, see oauthNoFlowDiag" is the part that earns its place.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded to just "Reported rather than refused; see oauthNoFlowDiag." b87282c

Comment thread compilers/openapi/internal/auth/auth.go Outdated
case soa.SecuritySchemeTypeOAuth2:
scheme.Kind = ir.AuthKindOAuth2
// An absent or empty flows object lowers to nil flows, which the caller
// reports (oauthNoFlowDiag).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new report reads an empty lowered flow list as "the document declares no flow", which holds only while presentFlows lists every flow field of soa.OAuthFlows. It does today: the five fields in v1.25.2 are all read, and I checked. But soa.SecurityScheme is held to its source struct by TestMechanismFieldNames_AccountForEverySourceField, and nothing holds presentFlows to soa.OAuthFlows.

If a library bump adds a flow field, a scheme declaring only that flow lowers to empty Flows. Before this PR that flow was dropped silently. Now it is dropped with a false "oauth2 scheme with no flow". A reflection test over soa.OAuthFlows beside the sibling guard would close it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TestPresentFlows_ReadsEverySourceFlowField in auth_internal_test.go, beside the sibling guard. It walks soa.OAuthFlows by reflection, sets each *soa.OAuthFlow field alone and requires presentFlows to return it. Any other field has to be listed as not-a-flow (only Extensions is). Commenting out the password candidate reddens it with "presentFlows does not read the Password flow". b87282c

},
},
{
name: "no type at all", entry: `{}`, refused: true,

@OmarAlJarrah OmarAlJarrah Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Kind == ir.AuthKindOAuth2 half of the new condition has no row of its own: every interned row in this table is oauth2. I planted if len(scheme.Flows) == 0 { (guard dropped) at this head. Exactly one test in the auth package reddens, TestLowerSecuritySchemes_ARefdEntryRecordsFieldsWhereTheyAreWritten, and only because its exact diagnostic list gains a warning for a mutualTLS alias. Six TestConformance subtests redden too (security-schemes, security-or-and, extension-promotion, extensions-x, neutral-naming, empty-names).

So the guard is held, but incidentally, and the PR's mutation list doesn't include this one. It also shows in the list's "fire only for absent flows" bullet, which credits ARefd... with catching that mutation. With the Kind guard kept (len(scheme.Flows) == 0 replaced by ss.GetFlows() == nil), ARefd... stays green and only the three oauth2 rows redden. ARefd... reddens only when the whole condition, Kind guard included, is replaced, so what it catches there is the missing guard.

One interned non-oauth2 row (apiKey or http) asserting messagesAt(diags, diag.OAuth2NoFlow) is empty would pin the guard where the rule lives.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a row of its own: an apiKey scheme carrying flows: {}. With the new GetFlows() != nil half, a plain apiKey could not reach the guard, so the row has to write a flows object. Dropping the Kind guard now reddens exactly that row of TestLowerSecuritySchemes_AnOAuth2SchemeWithNoFlowIsReported. The PR body's mutation list is corrected too. b87282c

}

// TestAuth_OAuthNoFlowsUnknownTypeAndGhostRef pins three entries beside a
// document that declares nothing wrong with them: an oauth2 scheme with no

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "beside a document that declares nothing wrong with them" isn't accurate, since two of the three entries are defective (the flowless oauth2 entry and the ghost $ref). "which is now reported" is change history rather than what the test pins. Further down, "the loader's other spelling" is confusing, because the entry here, {type: oauth2}, is the absent spelling itself. Something like "pins three entries a document can carry: an oauth2 scheme with no flow, reported at its own entry and interned anyway; ..." avoids both.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded along the lines you suggested: "pins three entries a document can carry: an oauth2 scheme with no flows object, interned with nil flows and reported only by the loader, …". The "loader's other spelling" passage is gone. That entry is the absent spelling, and the test now asserts it draws no oauth2-no-flow report. b87282c

// because the loader cannot place it: it accepts `flows: {}` and reports an
// absent `flows` with no pointer (GitHub #646).
//
// Reported, not refused as IncompleteSecurityScheme is: the IR states exactly

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The IR states exactly what the document said" is the argument for interning, but ir/auth.go:16 still documents AuthKindOAuth2 as "OAuth 2.0 with one or more flows". After this PR a flowless oauth2 scheme is a deliberately interned, documented shape, so that comment is now imprecise about a shape the compiler is designed to produce (and has produced silently since #4). Nothing in docs/ or irverify states or checks a flow count, so I found no emitter-facing contract broken. But a reader of ir.AuthKindOAuth2 is told the opposite of what this record says. Amending that line ("with zero or more flows; none when the document declares none, which a compiler reports") would keep the two in step.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amended as suggested: AuthKindOAuth2 is now "OAuth 2.0 with zero or more flows; none when the document declares none, which a compiler reports." b87282c

Comment thread compilers/openapi/internal/auth/auth.go Outdated
if scheme.Kind == ir.AuthKindOAuth2 && len(scheme.Flows) == 0 {
// Reported rather than refused — see oauthNoFlowDiag — and reported
// before the lines below, which is why those append into diags.
diags = append(diags, oauthNoFlowDiag(c, name, entry))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placement contradicts a rule this function states and a sibling test pins. lowerSecurityScheme's doc says entry is where the document names the scheme and places "anything said of the entry as a whole", while decl is "where the fields live". TestLowerSecuritySchemes_ARefdEntryRecordsFieldsWhereTheyAreWritten asserts "nothing is reported against a position the alias does not hold" and "two aliases of one declaration report it once", through the dedup on the whole diagnostic value in compile.Diags.

A flowless declaration is a fact about the declaration's text. This reports it at each alias's {$ref: ...} object, which holds no flow either way, and embeds the alias name in the message so the dedup cannot collapse the copies. One defect with N aliases gives N+1 warnings, and none is at the place to edit. The cited #107 concerns fabricated hoist pointers and doesn't mention security schemes.

Report at decl with a message that doesn't embed the entry name (the preserveUnreadFields messages don't either), and the copies dedup. Prototype: a target plus two aliases gives one warning at /components/securitySchemes/target, a three-deep alias chain gives one at the final declaration, and an unaliased scheme reports where it did before. TestLowerSecuritySchemes_AFlowlessRefEntryIsReportedAtEachEntry would then pin the rule as the function's doc states it. Switching the call to decl alone reddens only that test, so the PR's tests already pin the current choice.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. It's now reported at decl, and the message names no entry ("oauth2 security scheme declares no flow: no token endpoint is stated for it"), so the copies dedup. A target with two aliases gives one warning at /components/securitySchemes/target, and a three-deep alias chain gives one at the final declaration. The test is now TestLowerSecuritySchemes_AFlowlessDeclarationIsReportedOnceWhereItIsWritten, with both aliases declared before the target. Switching the call back to entry reddens it and TestConformance. The #107 citation is dropped from the code and the PR body. b87282c

// lowers to AuthKindOAuth2 with an empty flow list — which is why one rule
// covers them and why the report has to be sited here: the loader refuses only
// the absent spelling, and sites that finding at no pointer at all.
func TestLowerSecuritySchemes_AnOAuth2SchemeWithNoFlowIsReported(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing the repo's sweeps reach exercises this rule, so everything outside this package's unit tests is blind to it. I deleted the feature (if false && ... on the new branch, which compiles) and ran ./compilers/openapi, ./internal/harness/..., ./ir/..., ./pass/... and ./cmd/.... All stayed green, including TestConformance, the goldens and TestHarness_InRepoCorpus. Compiling the 154 specs under testdata/, none draws openapi/oauth2-no-flow; the four that declare type: oauth2 all declare a flow.

CLAUDE.md names the way to cover a new construct: add a spec the sweep reaches, so the no-panic, irverify, round-trip, determinism and order-invariance oracles run over it, and a golden pins the diagnostic's code, pointer and severity byte for byte. The PR's test plan checks the existing fixtures and leaves testdata/ alone, which is the gap. A conformance spec (testdata/conformance/openapi/: .yaml, .golden.json and a table row) should hold flows: {}, a flows object with only unknown keys, an aliased flowless scheme with the alias declared before its target (the order that would be wrong under a canonical-site bug), and a metadata-only 3.2.0 scheme as the silent control. I ran the harness on probes of these shapes and they pass, so adding them should be safe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added testdata/conformance/openapi/oauth2-no-flow.yaml with its golden and a table row (assertOAuth2NoFlow). It holds flows: {}, a flows object with only an unknown key, two aliases declared before their flowless target, and two silent controls: a 3.2.0 metadata-only scheme and a declared flow. morphic-harness passes on it. With the feature deleted (if false && …), TestConformance now reddens along with the unit tests. b87282c

@OmarAlJarrah

Copy link
Copy Markdown
Member

Verdict: not yet at the bar. The core is sound: flows: {} deserves a diagnostic, a warning is the right severity, interning rather than refusing is right, and the PR merges cleanly with current main with the full test suite green. It falls short of the best version in four ways, in order of weight. Each is detailed inline.

  1. False positive on a valid 3.2 document (auth.go:207). A metadata-only scheme warns that it "states no token endpoint", although oauth2MetadataUrl is the RFC 8414 metadata that states it. Exempt it.
  2. Duplicates the loader for the absent spelling (diag.go:252). One defect gets an error and a warning, and the exit code depends on the spelling. Report only the case the loader cannot see: flows present, declaring no flow.
  3. Placement contradicts lowerSecurityScheme's own rule (auth.go:122). N aliases give N+1 reports on {$ref: ...} objects, where the existing precedent is one report at the declaration.
  4. No sweep reaches it (auth_test.go:504). Deleting the feature leaves every corpus-level suite green. Add a conformance spec and golden.

Smaller: the IR's "one or more flows" text now misdescribes a shape this PR deliberately interns (diag.go:255), nothing holds presentFlows to the upstream flow fields (auth.go:231), the Kind guard is pinned only incidentally (auth_test.go:575), and three wording nits.

I prototyped 1 to 3. The whole production change is:

if scheme.Kind == ir.AuthKindOAuth2 && ss.GetFlows() != nil &&
	len(scheme.Flows) == 0 && scheme.OAuth2MetadataURL == "" {
	diags = append(diags, oauthNoFlowDiag(c, decl))
}
// oauthNoFlowDiag message: "oauth2 security scheme declares no flow: no token endpoint is stated for it"

Results against it: a target plus two aliases gives one warning at the target, a three-deep chain gives one at the final declaration, absent flows draws only the loader's error, {} and a flows object with only unknown keys give one warning, and a metadata-only 3.2.0 scheme, a declared flow and an apiKey scheme are silent. The tests that pin the per-entry placement and the metadata row change with it, which is intended.

The PR body should also drop two claims that this changes: "one rule for every spelling", and the #107 citation for per-entry reporting.

The oauth2-no-flow warning now fires only for what the loader cannot
see: a flows object that is present but declares no flow, on a scheme
with no oauth2MetadataUrl.

- An absent flows object is a REQUIRED-field error the loader already
  reports, with a line and column. Reporting it again gave one defect
  two severities and made the exit code depend on the spelling.
- A 3.2 metadata URL is RFC 8414 metadata, which states the token
  endpoint, so a metadata-only scheme is valid and usable.
- The report is placed at the declaration, and its message names no
  entry, so N aliases of one flowless declaration give one warning
  where the fields are written instead of N+1 at {$ref} objects.

A conformance spec puts the rule in reach of the corpus sweeps, a
reflection test holds presentFlows to every flow field soa.OAuthFlows
declares, an apiKey row pins the Kind guard, and ir.AuthKindOAuth2 no
longer claims one or more flows.
@fuad-daoud

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough pass. All four gaps and the smaller notes are addressed in b87282c, each one probed before the change:

  1. Metadata-only 3.2 scheme: exempt. A scheme with oauth2MetadataUrl is no longer reported. The table row compiles under 3.2.0 as the silent control, and the conformance spec carries the same control.
  2. Duplicating the loader: only a flows object that is present but names no flow is reported now. {type: oauth2} draws just the loader's required-field error, so the two spellings no longer get two severities.
  3. Placement: reported at the declaration, with a message that names no entry, so the copies dedup. A target with two aliases gives one warning at the target, and a three-deep alias chain gives one at the final declaration.
  4. Sweep coverage: new conformance spec oauth2-no-flow, with its golden and table row. Deleting the feature now reddens TestConformance, and morphic-harness passes on the spec.

The smaller notes:

  • ir.AuthKindOAuth2 now says zero or more flows.
  • TestPresentFlows_ReadsEverySourceFlowField holds presentFlows to soa.OAuthFlows by reflection.
  • An apiKey row with flows: {} pins the Kind guard.
  • Both wording nits are reworded.

The PR body is updated: it no longer claims one rule for every spelling or cites #107, and the mutation list is corrected. On the merge commit trailers: noted, and the squash body will be set by hand at merge time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openapi: an OAuth2 scheme with no flows compiles clean

2 participants